<?php
declare(strict_types=1);



use ErrorException;
use PDO;
use PDOException;
use PDOStatement;
use ServerApp\lib\core\ServerStation;
use Swoole\Coroutine\Mysql;
use txz\common\dao\base\mysql\CMysql;
use txz\common\third_lib\model\conf\DbConfig;

/**
 * Class Table
 * @package txz\common\third_lib\model\lib
 */
class Table extends AbstractTable
{
    private $sql;

    /**
     * @param $dbName
     * @param $table_name
     * @param $pre
     * @param $suffix
     * @return mixed
     */
    public function process($dbName, $table_name, $pre = '', $suffix = ''): void
    {
        $this->setPre($pre);
        $this->setSuffix($suffix);
        $this->setDbName($dbName);
        $this->setTableName(($pre ? $pre . '_' : '') . $table_name . ($suffix ? '_' . $suffix : ''));
    }

    /**
     * @return string
     */
    public function getLastSql(): string
    {
        return $this->sql;
    }

    /**
     * @return string
     */
    public function getLastInsertId(): string
    {
        return self::getCon()->lastInsertId();
    }

    /**
     * @return int
     */
    public function getErrorCode()
    {
        return self::getCon()->errorCode();
    }

    /**
     * @return string
     */
    public function getErrMsg()
    {
        return var_export(self::getCon()->errorInfo());
    }

    /**
     * @param $string
     * @return bool|string
     */
    public function escape_string($string)
    {
        return $this->getCon()->quote($string);
    }


    private function handle_db_error()
    {
        if ($this->getCon()->errorCode() === 2006) { // mysql gone away
            DbMgr::del($this->getTableName());
        }
    }

    /**
     * @return PDO
     */
    public function getCon(): PDO
    {
        return DbMgr::get($this);
    }


    /**
     * @param $_sql
     * @return bool
     * @throws ErrorException
     */
    public function execute($_sql)
    {
        try {
            $this->sql = $_sql;
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) {
                    return $client->query($this->sql);
                });
            } else {
                $m_stmt = $this->getCon()->prepare($_sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                $res = $m_stmt->execute();
                $m_stmt->closeCursor();
                return $res;
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            return false;
        }
    }

    /**
     * @param $where
     * @return bool|string
     */
    private function makeWhere(&$where)
    {
        $count = 65;
        if (is_array($where) && $where) {
            $str = 'WHERE ';
            foreach ($where as $k => $v) {
                if (is_array($v)) {
                    $fuhao = $v[0];
                    $v = $v[1];
                } else {
                    $fuhao = '=';
                }
                $tmp = ':a' . chr($count++);
                $str .= "`$k`" . $fuhao . $tmp . " AND ";
                $where[$tmp] = $v;
                unset($where[$k]);
            }
            $str = substr($str, 0, -5);
            return $str;
        } else {
            return '';
        }
    }

    /**
     * @param $where
     * @return bool|string
     */
    private function makeWhere1(array $where)
    {
        if (is_array($where) && $where) {
            $str = 'WHERE ';
            foreach ($where as $k => $v) {
                if (is_array($v)) {
                    $fuhao = $v[0];
                    $v = $v[1];
                } else {
                    $fuhao = '=';
                }
                if (is_string($v)) {
                    $v = "'" . $v . "'";
                }
                $str .= "`$k`" . $fuhao . $v . " AND ";
            }
            $str = substr($str, 0, -5);
            return $str;
        } else {
            return '';
        }
    }

    /**
     * @param $need
     * @return bool|string
     */
    private function makeNeed($need)
    {
        if (is_array($need) && $need) {
            $str = '';
            foreach ($need as $k => $v) {
                $str .= "`$v`,";
            }
            $str = substr($str, 0, -1);
            return $str;
        } else {
            return '*';
        }
    }

    /**
     * @param $group
     * @return string
     */
    public function makeGroup($group)
    {
        if ($group) {
            return "GROUP BY $group";
        } else {
            return '';
        }
    }

    /**
     * @param $limit
     * @return string
     */
    public function makeLimit($limit)
    {
        if ($limit && is_array($limit)) {
            return "LIMIT " . $limit[0] . "," . $limit[1];
        } else {
            return '';
        }
    }

    /**
     * @param $data
     * @return array|string
     */
    private function makeSetData($data)
    {
        $count = 65;
        if (is_object($data)) {
            $data = json_decode(json_encode($data), true);
        }
        if ($data && is_array($data)) {
            $str = 'SET ';
            foreach ($data as $k => $v) {
                $tmp = ":b" . chr($count++);
                $str .= "`{$k}`=$tmp,";
                $data[$tmp] = $v;
                unset($data[$k]);
            }
            $str = substr($str, 0, -1);
            return [$str, $data];
        } else {
            return '';
        }
    }

    /**
     * @param $data
     * @return bool|string
     */
    private function makeSetData1($data)
    {
        if ($data && is_array($data)) {
            $str = 'SET ';
            foreach ($data as $k => $v) {
                if (is_string($v)) {
                    $v = "'$v'";
                }
                $str .= "`{$k}`=$v,";
            }
            $str = substr($str, 0, -1);
            return $str;
        } else {
            return '';
        }
    }

    /**
     * @param PDOStatement $stmt
     * @param $data
     */
    private function bindParams(PDOStatement $stmt, $data)
    {
        if (is_array($data)) {
            foreach ($data as $k => $v) {
                if (is_array($v)) {
                    foreach ($v as $key => $val) {
                        $stmt->bindValue($key, $val);
                    }
                } else {
                    $stmt->bindValue($k, $v);
                }
            }
        }
    }

    /**
     * @param string $need
     * @param array $where
     * @param string $group
     * @param string $sort
     * @return array|mixed
     * @throws ErrorException
     */
    public function getObj($need = '', $where = [], $group = '', $sort = '')
    {
        try {
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) use ($need, $where, $group, $sort) {
                    $rows = $client->query($this->buildSqlSelect(true, $need, $where, $group, $sort, 1));
                    if ($rows) {
                        return $rows[0];
                    } else {
                        return $rows;
                    }
                });
            } else {
                $this->sql = $this->buildSqlSelect(false, $need, $where, $group, $sort);
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                if (is_array($where)) {
                    $this->bindParams($m_stmt, $where);
                }
                $m_stmt->execute();

                $res = $m_stmt->fetch(PDO::FETCH_ASSOC);
                $m_stmt->closeCursor();
                if (!is_array($res) || !$res) {
                    return [];
                } else {
                    return $res;
                }
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param bool $isBind
     * @param string $need
     * @param array $where
     * @param string $group
     * @param string $sort
     * @param string $limit
     * @return string
     * @throws ErrorException
     */
    public function buildSqlSelect(
        bool $isBind = false,
        $need = '',
        &$where = [],
        $group = '',
        $sort = '',
        $limit = ''
    ) {
        $table_name = $this->getTableName();
        if (!$table_name) {
            throw new ErrorException('no table');
        }

        if (!$isBind) {
            $str = $this->makeWhere($where);
        } else {
            $str = $this->makeWhere1($where);
        }
        if (!$str) {
            $str = '';
        }

        $group = $this->makeGroup($group);
        $sort = $this->makeOrder($sort);
        $limit = $this->makeLimit($limit);
        $need = $this->makeNeed($need);
        return "SELECT $need FROM `$table_name` {$str} $group {$sort} $limit";
    }

    /**
     * @param bool $isBind
     * @param array $data
     * @return string
     * @throws ErrorException
     */
    public function buildSqlInsert(bool $isBind = false, &$data = [])
    {
        $table_name = $this->getTableName();
        if (!$table_name) {
            throw new ErrorException('no table');
        }

        if (!$isBind) {
            $arr = $this->makeSetData($data);
            $str = $arr[0];
            $data = $arr[1];
        } else {
            $str = $this->makeSetData1($data);
        }

        if (!$str) {
            throw new ErrorException('cant find');
        }

        return "INSERT INTO `$table_name` $str";
    }

    /**
     * @param bool $isBind
     * @param array $data
     * @param array $where
     * @return string
     * @throws ErrorException
     */
    public function build_sql_update(bool $isBind = false, &$data=[], &$where=[])
    {
        $table_name = $this->getTableName();
        if (!$table_name) {
            throw new ErrorException('no table');
        }

        if (!$data) {
            throw new ErrorException('no need to update');
        }

        if (!$isBind) {
            $str = $this->makeWhere($where);
            $arr = $this->makeSetData($data);
            $set = $arr[0];
            $data = $arr[1];
        } else {
            $str = $this->makeWhere1($where);
            $set = $this->makeSetData1($data);
        }

        if (!$str) {
            throw new ErrorException('cant update');
        }

        return "UPDATE $table_name $set $str";
    }

    /**
     * @param bool $isBind
     * @param array $where
     * @return string
     * @throws ErrorException
     */
    public function build_sql_delete(bool $isBind = false, &$where=[])
    {
        $table_name = $this->getTableName();
        if (!$table_name) {
            throw new ErrorException('no table');
        }

        if (!$isBind) {
            $str = $this->makeWhere($where);
        } else {
            $str = $this->makeWhere1($where);
        }

        if (!$str) {
            throw new ErrorException('cant remove');
        }

        return "DELETE FROM `$table_name` {$str}";
    }

    /**
     * @param null $where
     * @return int
     * @throws ErrorException
     */
    public function getObjCount($where = null)
    {
        $table_name = $this->getTableName();
        try {
            if (!$table_name) {
                throw new ErrorException('no table');
            }

            $str = $this->makeWhere($where);
            $this->sql = "SELECT count(*) FROM `$table_name` $str";
            $m_stmt = $this->getCon()->prepare($this->sql);
            if (!$m_stmt) {
                throw new ErrorException($this->sql);
            }
            $this->bindParams($m_stmt, $where);
            $m_stmt->execute();
            $res = $m_stmt->fetch(PDO::FETCH_ASSOC);
            $m_stmt->closeCursor();
            if (!is_array($res) || !$res) {
                return 0;
            } else {
                return array_values($res)[0];
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sort
     * @return string
     */
    private function makeOrder($sort)
    {
        if ($sort) {
            if (is_array($sort)) {
                $sort = array_values($sort);
                return "ORDER BY `{$sort[0]}` ASC";
            } else {
                return "ORDER BY `{$sort}` DESC";
            }
        }
        return '';
    }

    /**
     * @param string $need
     * @param array $where
     * @param string $group
     * @param string $sort
     * @param string $limit
     * @return array
     * @throws ErrorException
     */
    public function getObjArr($need = '', $where = [], $group = '', $sort = '', $limit = '')
    {
        try {
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) use ($need, $where, $group, $sort, $limit) {
                    return $client->query($this->buildSqlSelect(true, $need, $where, $group, $sort, $limit));
                });
            } else {
                $this->sql = $this->buildSqlSelect(false, $need, $where, $group, $sort, $limit);
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                if ($where) {
                    $this->bindParams($m_stmt, $where);
                    unset($where);
                }

                $m_stmt->execute();
                $res = $m_stmt->fetchAll(PDO::FETCH_ASSOC);
                $m_stmt->closeCursor();
                if (!is_array($res) || !$res) {
                    return [];
                } else {
                    return $res;
                }
            }
        } catch (PDOException $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param array $data
     * @return bool
     * @throws ErrorException
     */
    public function addObj($data = [])
    {
        try {
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) use ($data) {
                    $sql = $this->buildSqlInsert(true, $data);
                    $res = $client->query($sql);
                    if (!$res) {
                        return 0;
                    } else {
                        return $client->insert_id;
                    }
                });
            } else {
                $this->sql = $this->buildSqlInsert(false, $data);
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->getCon()->errorInfo());
                }
                $this->bindParams($m_stmt, $data);
                $ret = $m_stmt->execute();
                $lastInsertId = -1;
                if ($ret) {
                    $lastInsertId = $this->getLastInsertId();
                }
                $m_stmt->closeCursor();
                return $lastInsertId;
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param string $data
     * @return bool
     * @throws ErrorException
     */
    public function replaceObj($data = '')
    {
        $table_name = $this->getTableName();
        try {
            if (!$table_name) {
                throw new ErrorException('no table');
            }

            $arr = $this->makeSetData($data);
            $insert = $arr[0];
            $data = $arr[1];

            $this->sql = "REPLACE INTO `$table_name` $insert";

            $m_stmt = $this->getCon()->prepare($this->sql);
            if (!$m_stmt) {
                throw new ErrorException($this->sql);
            }
            $this->bindParams($m_stmt, $data);
            $ret = $m_stmt->execute();
            $m_stmt->closeCursor();
            return $ret;
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param array $data
     * @return bool
     * @throws ErrorException
     */
    public function replaceObjs($data = [])
    {
        $table_name = $this->getTableName();
        try {
            if (!$table_name) {
                throw new ErrorException('no table');
            }

            $arr = $this->makeSetData($data);
            $insert = $arr[0];
            $data = $arr[1];

            $this->sql = "REPLACE INTO `$table_name` $insert";

            $m_stmt = $this->getCon()->prepare($this->sql);
            if (!$m_stmt) {
                throw new ErrorException($this->sql);
            }
            $this->bindParams($m_stmt, $data);
            $ret = $m_stmt->execute();
            $m_stmt->closeCursor();
            return $ret;
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param array $where
     * @return bool
     * @throws ErrorException
     */
    public function removeObj($where = [])
    {
        try {
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) use ($where) {
                    $res = $client->query($this->build_sql_delete(true, $where));
                    if (!$res) {
                        return 0;
                    } else {
                        return $client->insert_id;
                    }
                });
            } else {
                $this->sql = $this->build_sql_delete(false, $where);
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                $this->bindParams($m_stmt, [$where]);

                $ret = $m_stmt->execute();
                $m_stmt->closeCursor();
                return $ret;
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param array $data
     * @param array $where
     * @return bool
     * @throws ErrorException
     */
    public function updateObj($data = [], $where = [])
    {
        try {
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) use ($data, $where) {
                    $res = $client->query($this->build_sql_update(true, $data, $where));
                    if (!$res) {
                        return 0;
                    } else {
                        return $client->insert_id;
                    }
                });
            } else {
                $this->sql = $this->build_sql_update(false, $data, $where);
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException(
                        $this->getCon()->errorCode() . $this->getCon()->errorInfo()
                    );
                }
                $this->bindParams($m_stmt, [$data, $where]);
                $ret = $m_stmt->execute();
                $m_stmt->closeCursor();
                return $ret;
            }
        } catch (PDOException  $e) {
            $this->handle_db_error();
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sql
     * @return mixed
     * @throws ErrorException
     */
    public function getObjBySql($sql)
    {
        try {
            $this->sql = $sql;
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) {
                    $res = $client->query($this->sql);
                    if ($res) {
                        return $res[0];
                    } else {
                        return [];
                    }
                });
            } else {
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                $m_stmt->execute();
                $res = $m_stmt->fetch(PDO::FETCH_ASSOC);
                $m_stmt->closeCursor();
                return $res;
            }
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sql
     * @return array
     * @throws ErrorException
     */
    public function getObjArrBySql($sql)
    {
        try {
            $this->sql = $sql;
            if (!$this->con && ServerStation::$wId !== null) {
                return MysqlPool::exec(function (Mysql $client) {
                    return $client->query($this->sql);
                });
            } else {
                $m_stmt = $this->getCon()->prepare($this->sql);
                if (!$m_stmt) {
                    throw new ErrorException($this->sql);
                }
                $m_stmt->execute();
                $res = $m_stmt->fetchAll(PDO::FETCH_ASSOC);
                $m_stmt->closeCursor();
                return $res;
            }
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $arr
     * @return bool
     * @internal param $call_back
     */
    public function transaction($arr): bool
    {
        $this->getCon()->beginTransaction();
        try {
            foreach ($arr as $res) {
                if (gettype($res) !== 'object') {
                    throw new ErrorException();
                }
                $res = (bool)$res();
                if (!$res) {
                    throw new ErrorException();
                }
            }
            $this->getCon()->commit();
            return true;
        } catch (PDOException $e) {
            $this->handle_db_error();
            $this->getCon()->rollBack();
        } catch (ErrorException $e) {
            $this->getCon()->rollBack();
        }
        return false;
    }

    /**
     * @param $key
     * @return PDO
     * @throws ErrorException
     */
    public function buildConnection($key)
    {
        $db = new CMysql();

        if (false === $db->init(
                DbConfig::getInstance()->getHost($key),
                DbConfig::getInstance()->getUser($key),
                DbConfig::getInstance()->getPwd($key),
                $key
            )
        ) {
            throw new ErrorException("connect db $key error:" . $db->errMsg());
        }
        return $db->m_pdo;
    }
}
